//! Helpers for rendering interpreter values back into readable Roc syntax.

const std = @import("std");
const builtin = @import("builtin");
const types = @import("types");
const can = @import("can");
const layout = @import("layout");
const builtins = @import("builtins");
const StackValue = @import("StackValue.zig");
const RocDec = builtins.dec.RocDec;
const TypeScope = types.TypeScope;

fn toVarRange(range: anytype) types.Var.SafeList.Range {
    const RangeType = types.Var.SafeList.Range;
    if (comptime @hasField(@TypeOf(range), "nonempty")) {
        return @field(range, "nonempty");
    }
    return @as(RangeType, range);
}

/// Callback function type for checking and rendering nominal types with custom to_inspect methods.
/// Returns the rendered string if the type has a to_inspect method, null otherwise.
/// Ownership of the returned string is transferred to the caller.
pub const ToInspectCallback = *const fn (ctx: *anyopaque, value: StackValue, rt_var: types.Var) ?[]u8;

/// Shared rendering context that provides allocator, module environment, and runtime caches.
pub const RenderCtx = struct {
    allocator: std.mem.Allocator,
    env: *can.ModuleEnv,
    runtime_types: *types.store.Store,
    layout_store: *layout.Store,
    type_scope: *const TypeScope,
    /// Optional callback for handling nominal types with custom to_inspect methods.
    /// If set, this callback will be invoked when rendering nominal type values.
    to_inspect_callback: ?ToInspectCallback = null,
    /// Opaque context pointer passed to the to_inspect callback.
    callback_ctx: ?*anyopaque = null,
};

/// Render `value` using the supplied runtime type variable, following alias/nominal backing.
pub fn renderValueRocWithType(ctx: *RenderCtx, value: StackValue, rt_var: types.Var) ![]u8 {
    const gpa = ctx.allocator;
    var resolved = ctx.runtime_types.resolveVar(rt_var);

    // Check layout first for special rendering cases
    // Str has .str layout, Bool has .int .u8 layout
    if (value.layout.tag == .scalar) {
        const scalar = value.layout.data.scalar;
        if (scalar.tag == .str) {
            // Render strings with quotes
            const rs: *const builtins.str.RocStr = @ptrCast(@alignCast(value.ptr.?));
            const s = rs.asSlice();
            var buf = std.array_list.AlignedManaged(u8, null).init(gpa);
            errdefer buf.deinit();
            try buf.append('"');
            for (s) |ch| {
                switch (ch) {
                    '\\' => try buf.appendSlice("\\\\"),
                    '"' => try buf.appendSlice("\\\""),
                    else => try buf.append(ch),
                }
            }
            try buf.append('"');
            return buf.toOwnedSlice();
        }
    }

    // unwrap aliases/nominals, but check for to_inspect callbacks on nominal types first
    unwrap: while (true) {
        switch (resolved.desc.content) {
            .alias => |al| {
                const backing = ctx.runtime_types.getAliasBackingVar(al);
                resolved = ctx.runtime_types.resolveVar(backing);
            },
            .structure => |st| switch (st) {
                .nominal_type => |nt| {
                    // Check if there's a to_inspect callback for this nominal type
                    if (ctx.to_inspect_callback) |callback| {
                        if (ctx.callback_ctx) |cb_ctx| {
                            // The callback returns the rendered string if the type has to_inspect,
                            // null otherwise
                            if (callback(cb_ctx, value, rt_var)) |rendered| {
                                return rendered;
                            }
                        }
                    }
                    // No custom to_inspect, unwrap to backing type
                    const backing = ctx.runtime_types.getNominalBackingVar(nt);
                    resolved = ctx.runtime_types.resolveVar(backing);
                },
                else => break :unwrap,
            },
            else => break :unwrap,
        }
    }

    if (resolved.desc.content == .structure) switch (resolved.desc.content.structure) {
        .tag_union => |tu| {
            const tags = ctx.runtime_types.getTagsSlice(tu.tags);
            var tag_index: usize = 0;
            var have_tag = false;
            if (value.layout.tag == .zst) {
                // Zero-sized tag union - must be the first (and only) tag with no payload
                if (tags.len > 0) {
                    const tag_name = ctx.env.getIdent(tags.items(.name)[0]);
                    var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                    errdefer out.deinit();
                    try out.appendSlice(tag_name);
                    return out.toOwnedSlice();
                }
            } else if (value.layout.tag == .scalar) {
                if (value.layout.data.scalar.tag == .int) {
                    // Only treat as tag if value fits in usize (valid tag discriminants are small)
                    if (std.math.cast(usize, value.asI128())) |idx| {
                        tag_index = idx;
                        have_tag = true;
                    }
                }
                if (have_tag and tag_index < tags.len) {
                    const tag_name = ctx.env.getIdent(tags.items(.name)[tag_index]);
                    var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                    errdefer out.deinit();
                    try out.appendSlice(tag_name);
                    return out.toOwnedSlice();
                }
            } else if (value.layout.tag == .tuple) {
                // Tag union stored as tuple: (payload, tag_index) or (payload_tuple, tag_index)
                // The last element of the tuple is the tag discriminant
                var tup_acc = try value.asTuple(ctx.layout_store);
                const count = tup_acc.getElementCount();
                if (count > 0) {
                    // Get tag index from the last element
                    // rt_var not needed for tag discriminant access (it's always an integer)
                    const tag_elem = try tup_acc.getElement(count - 1, undefined);
                    if (tag_elem.layout.tag == .scalar and tag_elem.layout.data.scalar.tag == .int) {
                        if (std.math.cast(usize, tag_elem.asI128())) |tag_idx| {
                            tag_index = tag_idx;
                            have_tag = true;
                        }
                    }
                }
                if (have_tag and tag_index < tags.len) {
                    const tag_name = ctx.env.getIdent(tags.items(.name)[tag_index]);
                    var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                    errdefer out.deinit();
                    try out.appendSlice(tag_name);
                    const args_range = tags.items(.args)[tag_index];
                    const arg_vars = ctx.runtime_types.sliceVars(toVarRange(args_range));
                    if (arg_vars.len > 0) {
                        try out.append('(');
                        if (arg_vars.len == 1) {
                            // Single payload: first element
                            // Get the correct layout from the type variable, not the payload union layout
                            const arg_var = arg_vars[0];
                            const payload_elem = try tup_acc.getElement(0, arg_var);
                            const layout_idx = try ctx.layout_store.addTypeVar(arg_var, ctx.type_scope);
                            const arg_layout = ctx.layout_store.getLayout(layout_idx);
                            const payload_value = StackValue{
                                .layout = arg_layout,
                                .ptr = payload_elem.ptr,
                                .is_initialized = payload_elem.is_initialized,
                                .rt_var = arg_var,
                            };
                            const rendered = try renderValueRocWithType(ctx, payload_value, arg_var);
                            defer gpa.free(rendered);
                            try out.appendSlice(rendered);
                        } else {
                            // Multiple payloads: first element is a nested tuple containing all payload args
                            // rt_var undefined for tuple access (we have the individual element types)
                            const payload_elem = try tup_acc.getElement(0, undefined);
                            if (payload_elem.layout.tag == .tuple) {
                                var payload_tup = try payload_elem.asTuple(ctx.layout_store);
                                var j: usize = 0;
                                while (j < arg_vars.len) : (j += 1) {
                                    const elem_value = try payload_tup.getElement(j, arg_vars[j]);
                                    const rendered = try renderValueRocWithType(ctx, elem_value, arg_vars[j]);
                                    defer gpa.free(rendered);
                                    try out.appendSlice(rendered);
                                    if (j + 1 < arg_vars.len) try out.appendSlice(", ");
                                }
                            } else {
                                // Fallback: render the raw payload
                                const rendered = try renderValueRoc(ctx, payload_elem);
                                defer gpa.free(rendered);
                                try out.appendSlice(rendered);
                            }
                        }
                        try out.append(')');
                    }
                    return out.toOwnedSlice();
                }
            } else if (value.layout.tag == .record) {
                var acc = try value.asRecord(ctx.layout_store);
                if (acc.findFieldIndex(ctx.env.idents.tag)) |idx| {
                    const field_rt = try ctx.runtime_types.fresh();
                    const tag_field = try acc.getFieldByIndex(idx, field_rt);
                    if (tag_field.layout.tag == .scalar and tag_field.layout.data.scalar.tag == .int) {
                        const tmp_sv = StackValue{ .layout = tag_field.layout, .ptr = tag_field.ptr, .is_initialized = true, .rt_var = undefined };
                        // Only treat as tag if value fits in usize (valid tag discriminants are small)
                        if (std.math.cast(usize, tmp_sv.asI128())) |tag_idx| {
                            tag_index = tag_idx;
                            have_tag = true;
                        }
                    }
                }
                if (have_tag and tag_index < tags.len) {
                    const tag_name = ctx.env.getIdent(tags.items(.name)[tag_index]);
                    var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                    errdefer out.deinit();
                    try out.appendSlice(tag_name);
                    if (acc.findFieldIndex(ctx.env.idents.payload)) |pidx| {
                        const field_rt = try ctx.runtime_types.fresh();
                        const payload = try acc.getFieldByIndex(pidx, field_rt);
                        const args_range = tags.items(.args)[tag_index];
                        const arg_vars = ctx.runtime_types.sliceVars(toVarRange(args_range));
                        if (arg_vars.len > 0) {
                            try out.append('(');
                            if (arg_vars.len == 1) {
                                const arg_var = arg_vars[0];
                                const layout_idx = try ctx.layout_store.addTypeVar(arg_var, ctx.type_scope);
                                const arg_layout = ctx.layout_store.getLayout(layout_idx);
                                const payload_value = StackValue{
                                    .layout = arg_layout,
                                    .ptr = payload.ptr,
                                    .is_initialized = payload.is_initialized,
                                    .rt_var = arg_var,
                                };
                                const rendered = try renderValueRocWithType(ctx, payload_value, arg_var);
                                defer gpa.free(rendered);
                                try out.appendSlice(rendered);
                            } else {
                                var elem_layouts = try ctx.allocator.alloc(layout.Layout, arg_vars.len);
                                defer ctx.allocator.free(elem_layouts);
                                var i: usize = 0;
                                while (i < arg_vars.len) : (i += 1) {
                                    const idx = try ctx.layout_store.addTypeVar(arg_vars[i], ctx.type_scope);
                                    elem_layouts[i] = ctx.layout_store.getLayout(idx);
                                }
                                const tuple_idx = try ctx.layout_store.putTuple(elem_layouts);
                                const tuple_layout = ctx.layout_store.getLayout(tuple_idx);
                                const tuple_size = ctx.layout_store.layoutSize(tuple_layout);
                                var tuple_value = StackValue{
                                    .layout = tuple_layout,
                                    .ptr = payload.ptr,
                                    .is_initialized = payload.is_initialized,
                                    .rt_var = undefined, // not needed - type known from layout
                                };
                                if (tuple_size == 0 or payload.ptr == null) {
                                    var j: usize = 0;
                                    while (j < arg_vars.len) : (j += 1) {
                                        const rendered = try renderValueRocWithType(
                                            ctx,
                                            StackValue{
                                                .layout = elem_layouts[j],
                                                .ptr = null,
                                                .is_initialized = true,
                                                .rt_var = arg_vars[j],
                                            },
                                            arg_vars[j],
                                        );
                                        defer gpa.free(rendered);
                                        try out.appendSlice(rendered);
                                        if (j + 1 < arg_vars.len) try out.appendSlice(", ");
                                    }
                                } else {
                                    var tup_acc = try tuple_value.asTuple(ctx.layout_store);
                                    var j: usize = 0;
                                    while (j < arg_vars.len) : (j += 1) {
                                        const sorted_idx = tup_acc.findElementIndexByOriginal(j) orelse return error.TypeMismatch;
                                        const elem_value = try tup_acc.getElement(sorted_idx, arg_vars[j]);
                                        const rendered = try renderValueRocWithType(ctx, elem_value, arg_vars[j]);
                                        defer gpa.free(rendered);
                                        try out.appendSlice(rendered);
                                        if (j + 1 < arg_vars.len) try out.appendSlice(", ");
                                    }
                                }
                            }
                            try out.append(')');
                        }
                    }
                    return out.toOwnedSlice();
                }
            } else if (value.layout.tag == .tag_union) {
                // Tag union with new proper layout: payload at offset 0, discriminant at discriminant_offset
                const tu_data = ctx.layout_store.getTagUnionData(value.layout.data.tag_union.idx);
                if (value.ptr) |ptr| {
                    const base_ptr: [*]u8 = @ptrCast(ptr);
                    const disc_ptr = base_ptr + tu_data.discriminant_offset;
                    // Read discriminant based on its size
                    const discriminant: usize = switch (tu_data.discriminant_size) {
                        1 => @as(*const u8, @ptrCast(disc_ptr)).*,
                        2 => @as(*const u16, @ptrCast(@alignCast(disc_ptr))).*,
                        4 => @as(*const u32, @ptrCast(@alignCast(disc_ptr))).*,
                        8 => @intCast(@as(*const u64, @ptrCast(@alignCast(disc_ptr))).*),
                        else => 0,
                    };
                    tag_index = discriminant;
                    have_tag = true;
                }
                if (have_tag and tag_index < tags.len) {
                    const tag_name = ctx.env.getIdent(tags.items(.name)[tag_index]);
                    var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                    errdefer out.deinit();
                    try out.appendSlice(tag_name);
                    const args_range = tags.items(.args)[tag_index];
                    const arg_vars = ctx.runtime_types.sliceVars(toVarRange(args_range));
                    if (arg_vars.len > 0) {
                        try out.append('(');
                        // Payload is at offset 0
                        const payload_ptr: *anyopaque = @ptrCast(value.ptr.?);
                        if (arg_vars.len == 1) {
                            const arg_var = arg_vars[0];
                            const layout_idx = try ctx.layout_store.addTypeVar(arg_var, ctx.type_scope);
                            const arg_layout = ctx.layout_store.getLayout(layout_idx);
                            const payload_value = StackValue{
                                .layout = arg_layout,
                                .ptr = payload_ptr,
                                .is_initialized = true,
                                .rt_var = arg_var,
                            };
                            const rendered = try renderValueRocWithType(ctx, payload_value, arg_var);
                            defer gpa.free(rendered);
                            try out.appendSlice(rendered);
                        } else {
                            // Multiple payloads: create a tuple layout from arg types
                            var elem_layouts = try ctx.allocator.alloc(layout.Layout, arg_vars.len);
                            defer ctx.allocator.free(elem_layouts);
                            var i: usize = 0;
                            while (i < arg_vars.len) : (i += 1) {
                                const idx = try ctx.layout_store.addTypeVar(arg_vars[i], ctx.type_scope);
                                elem_layouts[i] = ctx.layout_store.getLayout(idx);
                            }
                            const tuple_idx = try ctx.layout_store.putTuple(elem_layouts);
                            const tuple_layout = ctx.layout_store.getLayout(tuple_idx);
                            const tuple_size = ctx.layout_store.layoutSize(tuple_layout);
                            if (tuple_size == 0) {
                                var j: usize = 0;
                                while (j < arg_vars.len) : (j += 1) {
                                    const rendered = try renderValueRocWithType(
                                        ctx,
                                        StackValue{
                                            .layout = elem_layouts[j],
                                            .ptr = null,
                                            .is_initialized = true,
                                            .rt_var = arg_vars[j],
                                        },
                                        arg_vars[j],
                                    );
                                    defer gpa.free(rendered);
                                    try out.appendSlice(rendered);
                                    if (j + 1 < arg_vars.len) try out.appendSlice(", ");
                                }
                            } else {
                                const tuple_value = StackValue{
                                    .layout = tuple_layout,
                                    .ptr = payload_ptr,
                                    .is_initialized = true,
                                    .rt_var = undefined, // not needed - type known from layout
                                };
                                var tup_acc = try tuple_value.asTuple(ctx.layout_store);
                                var j: usize = 0;
                                while (j < arg_vars.len) : (j += 1) {
                                    const sorted_idx = tup_acc.findElementIndexByOriginal(j) orelse return error.TypeMismatch;
                                    const elem_value = try tup_acc.getElement(sorted_idx, arg_vars[j]);
                                    const rendered = try renderValueRocWithType(ctx, elem_value, arg_vars[j]);
                                    defer gpa.free(rendered);
                                    try out.appendSlice(rendered);
                                    if (j + 1 < arg_vars.len) try out.appendSlice(", ");
                                }
                            }
                        }
                        try out.append(')');
                    }
                    return out.toOwnedSlice();
                }
            }
        },
        .nominal_type => |nominal| {
            if (nominal.ident.ident_idx == ctx.env.idents.box) {
                const args_range = nominal.vars;
                const arg_vars = ctx.runtime_types.sliceVars(toVarRange(args_range));
                if (arg_vars.len != 1) return error.TypeMismatch;
                const payload_var = arg_vars[0];

                var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                errdefer out.deinit();
                try out.appendSlice("Box(");

                const payload_layout_idx = try ctx.layout_store.addTypeVar(payload_var, ctx.type_scope);
                const payload_layout = ctx.layout_store.getLayout(payload_layout_idx);
                const payload_size = ctx.layout_store.layoutSize(payload_layout);

                var payload_value = StackValue{
                    .layout = payload_layout,
                    .ptr = null,
                    .is_initialized = true,
                    .rt_var = payload_var,
                };

                switch (value.layout.tag) {
                    .box => {
                        const elem_layout = ctx.layout_store.getLayout(value.layout.data.box);
                        const data_ptr_opt = value.boxDataPointer() orelse return error.TypeMismatch;
                        if (!std.meta.eql(elem_layout, payload_layout)) {
                            return error.TypeMismatch;
                        }
                        if (payload_size > 0) {
                            payload_value.ptr = @as(*anyopaque, @ptrFromInt(@intFromPtr(data_ptr_opt)));
                        }
                        const rendered_payload = try renderValueRocWithType(ctx, payload_value, payload_var);
                        defer gpa.free(rendered_payload);
                        try out.appendSlice(rendered_payload);
                    },
                    .box_of_zst => {
                        if (payload_size != 0) return error.TypeMismatch;
                        const rendered_payload = try renderValueRocWithType(ctx, payload_value, payload_var);
                        defer gpa.free(rendered_payload);
                        try out.appendSlice(rendered_payload);
                    },
                    else => return error.TypeMismatch,
                }

                try out.append(')');
                return out.toOwnedSlice();
            }
        },
        .record => |rec| {
            // Gather all record fields by following the extension chain
            var all_fields = std.array_list.AlignedManaged(types.RecordField, null).init(gpa);
            defer all_fields.deinit();

            // Add fields from the initial record
            const initial_fields = ctx.runtime_types.getRecordFieldsSlice(rec.fields);
            for (initial_fields.items(.name), initial_fields.items(.var_)) |name, var_| {
                try all_fields.append(.{ .name = name, .var_ = var_ });
            }

            // Follow the extension chain to gather all fields
            var ext = rec.ext;
            var is_valid = true;
            while (is_valid) {
                const ext_resolved = ctx.runtime_types.resolveVar(ext);
                switch (ext_resolved.desc.content) {
                    .structure => |flat_type| switch (flat_type) {
                        .record => |ext_record| {
                            const ext_fields = ctx.runtime_types.getRecordFieldsSlice(ext_record.fields);
                            for (ext_fields.items(.name), ext_fields.items(.var_)) |name, var_| {
                                try all_fields.append(.{ .name = name, .var_ = var_ });
                            }
                            ext = ext_record.ext;
                        },
                        .empty_record => break, // Reached the end of the extension chain
                        else => {
                            is_valid = false;
                        },
                    },
                    .alias => |alias| {
                        // Follow alias to its backing type
                        ext = ctx.runtime_types.getAliasBackingVar(alias);
                    },
                    else => {
                        is_valid = false;
                    },
                }
            }

            if (is_valid and all_fields.items.len > 0) {
                var out = std.array_list.AlignedManaged(u8, null).init(gpa);
                errdefer out.deinit();
                try out.appendSlice("{ ");
                var acc = try value.asRecord(ctx.layout_store);
                for (all_fields.items, 0..) |f, i| {
                    const name_text = ctx.env.getIdent(f.name);
                    try out.appendSlice(name_text);
                    try out.appendSlice(": ");
                    const idx = acc.findFieldIndex(f.name) orelse {
                        std.debug.panic("Record field not found in layout: type says field '{s}' exists but layout doesn't have it", .{name_text});
                    };
                    const field_rt = try ctx.runtime_types.fresh();
                    const field_val = try acc.getFieldByIndex(idx, field_rt);
                    const rendered = try renderValueRocWithType(ctx, field_val, f.var_);
                    defer gpa.free(rendered);
                    try out.appendSlice(rendered);
                    if (i + 1 < all_fields.items.len) try out.appendSlice(", ");
                }
                try out.appendSlice(" }");
                return out.toOwnedSlice();
            }
            // Fall through to renderValueRoc which can use layout info
        },
        .fn_pure, .fn_effectful, .fn_unbound => {
            return try gpa.dupe(u8, "<function>");
        },
        else => {},
    };
    return try renderValueRoc(ctx, value);
}

/// Render `value` using only its layout (without additional type information).
pub fn renderValueRoc(ctx: *RenderCtx, value: StackValue) ![]u8 {
    const gpa = ctx.allocator;
    if (value.layout.tag == .scalar) {
        const scalar = value.layout.data.scalar;
        switch (scalar.tag) {
            .str => {
                const rs: *const builtins.str.RocStr = @ptrCast(@alignCast(value.ptr.?));
                const s = rs.asSlice();
                var buf = std.array_list.AlignedManaged(u8, null).init(gpa);
                errdefer buf.deinit();
                try buf.append('"');
                for (s) |ch| {
                    switch (ch) {
                        '\\' => try buf.appendSlice("\\\\"),
                        '"' => try buf.appendSlice("\\\""),
                        else => try buf.append(ch),
                    }
                }
                try buf.append('"');
                return buf.toOwnedSlice();
            },
            .int => {
                const i = value.asI128();
                return try std.fmt.allocPrint(gpa, "{d}", .{i});
            },
            .frac => {
                std.debug.assert(value.ptr != null);
                return switch (scalar.data.frac) {
                    .f32 => {
                        const ptr = @as(*const f32, @ptrCast(@alignCast(value.ptr.?)));
                        return try std.fmt.allocPrint(gpa, "{d}", .{@as(f64, ptr.*)});
                    },
                    .f64 => {
                        const ptr = @as(*const f64, @ptrCast(@alignCast(value.ptr.?)));
                        return try std.fmt.allocPrint(gpa, "{d}", .{ptr.*});
                    },
                    .dec => {
                        const ptr = @as(*const RocDec, @ptrCast(@alignCast(value.ptr.?)));
                        return try renderDecimal(gpa, ptr.*);
                    },
                };
            },
            else => {},
        }
    }
    if (value.layout.tag == .tuple) {
        var out = std.array_list.AlignedManaged(u8, null).init(gpa);
        errdefer out.deinit();
        try out.append('(');
        var acc = try value.asTuple(ctx.layout_store);
        const count = acc.getElementCount();
        var i: usize = 0;
        while (i < count) : (i += 1) {
            // rt_var undefined (no type info available in this context)
            const elem = try acc.getElement(i, undefined);
            const rendered = try renderValueRoc(ctx, elem);
            defer gpa.free(rendered);
            try out.appendSlice(rendered);
            if (i + 1 < count) try out.appendSlice(", ");
        }
        try out.append(')');
        return out.toOwnedSlice();
    }
    if (value.layout.tag == .list) {
        var out = std.array_list.AlignedManaged(u8, null).init(gpa);
        errdefer out.deinit();
        const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(value.ptr.?));
        const len = roc_list.len();
        try out.append('[');
        if (len > 0) {
            const elem_layout_idx = value.layout.data.list;
            const elem_layout = ctx.layout_store.getLayout(elem_layout_idx);
            const elem_size = ctx.layout_store.layoutSize(elem_layout);
            var i: usize = 0;
            while (i < len) : (i += 1) {
                if (roc_list.bytes) |bytes| {
                    const elem_ptr: *anyopaque = @ptrCast(bytes + i * elem_size);
                    const elem_val = StackValue{ .layout = elem_layout, .ptr = elem_ptr, .is_initialized = true, .rt_var = undefined };
                    const rendered = try renderValueRoc(ctx, elem_val);
                    defer gpa.free(rendered);
                    try out.appendSlice(rendered);
                    if (i + 1 < len) try out.appendSlice(", ");
                }
            }
        }
        try out.append(']');
        return out.toOwnedSlice();
    }
    if (value.layout.tag == .list_of_zst) {
        // list_of_zst is used for empty lists - render as []
        const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(value.ptr.?));
        const len = roc_list.len();
        if (len == 0) {
            return try gpa.dupe(u8, "[]");
        }
        // Non-empty list of ZST - show count
        return try std.fmt.allocPrint(gpa, "[<{d} zero-sized elements>]", .{len});
    }
    if (value.layout.tag == .record) {
        var out = std.array_list.AlignedManaged(u8, null).init(gpa);
        errdefer out.deinit();
        const rec_data = ctx.layout_store.getRecordData(value.layout.data.record.idx);
        if (rec_data.fields.count == 0) {
            try out.appendSlice("{}");
            return out.toOwnedSlice();
        }
        try out.appendSlice("{ ");
        const fields = ctx.layout_store.record_fields.sliceRange(rec_data.getFields());
        var i: usize = 0;
        while (i < fields.len) : (i += 1) {
            const fld = fields.get(i);
            const name_text = ctx.env.getIdent(fld.name);
            try out.appendSlice(name_text);
            try out.appendSlice(": ");
            const offset = ctx.layout_store.getRecordFieldOffset(value.layout.data.record.idx, @intCast(i));
            const field_layout = ctx.layout_store.getLayout(fld.layout);
            const base_ptr: [*]u8 = @ptrCast(@alignCast(value.ptr.?));
            const field_ptr: *anyopaque = @ptrCast(base_ptr + offset);
            const field_val = StackValue{ .layout = field_layout, .ptr = field_ptr, .is_initialized = true, .rt_var = undefined };
            const rendered = try renderValueRoc(ctx, field_val);
            defer gpa.free(rendered);
            try out.appendSlice(rendered);
            if (i + 1 < fields.len) try out.appendSlice(", ");
        }
        try out.appendSlice(" }");
        return out.toOwnedSlice();
    }
    if (value.layout.tag == .tag_union) {
        // Layout-only fallback for tag_union: show discriminant and raw payload
        const tu_data = ctx.layout_store.getTagUnionData(value.layout.data.tag_union.idx);
        var out = std.array_list.AlignedManaged(u8, null).init(gpa);
        errdefer out.deinit();
        if (value.ptr) |ptr| {
            const base_ptr: [*]u8 = @ptrCast(ptr);
            const disc_ptr = base_ptr + tu_data.discriminant_offset;
            const discriminant: usize = switch (tu_data.discriminant_size) {
                1 => @as(*const u8, @ptrCast(disc_ptr)).*,
                2 => @as(*const u16, @ptrCast(@alignCast(disc_ptr))).*,
                4 => @as(*const u32, @ptrCast(@alignCast(disc_ptr))).*,
                8 => @intCast(@as(*const u64, @ptrCast(@alignCast(disc_ptr))).*),
                else => 0,
            };
            try std.fmt.format(out.writer(), "<tag_union variant={d}>", .{discriminant});
        } else {
            try out.appendSlice("<tag_union>");
        }
        return out.toOwnedSlice();
    }
    return try std.fmt.allocPrint(gpa, "<unsupported>", .{});
}

fn renderDecimal(gpa: std.mem.Allocator, dec: RocDec) ![]u8 {
    if (dec.num == 0) {
        return try gpa.dupe(u8, "0");
    }

    var out = std.array_list.AlignedManaged(u8, null).init(gpa);
    errdefer out.deinit();

    var num = dec.num;
    if (num < 0) {
        try out.append('-');
        num = -num;
    }

    const one = RocDec.one_point_zero_i128;
    const integer_part = @divTrunc(num, one);
    const fractional_part = @rem(num, one);

    try std.fmt.format(out.writer(), "{d}", .{integer_part});

    if (fractional_part == 0) {
        return out.toOwnedSlice();
    }

    try out.writer().writeByte('.');

    const decimal_places: usize = @as(usize, RocDec.decimal_places);
    var digits: [decimal_places]u8 = undefined;
    @memset(digits[0..], '0');
    var remaining = fractional_part;
    var idx: usize = decimal_places;
    while (idx > 0) : (idx -= 1) {
        const digit: u8 = @intCast(@mod(remaining, 10));
        digits[idx - 1] = digit + '0';
        remaining = @divTrunc(remaining, 10);
    }

    var end: usize = decimal_places;
    while (end > 1 and digits[end - 1] == '0') {
        end -= 1;
    }

    try out.writer().writeAll(digits[0..end]);
    return out.toOwnedSlice();
}
